跳到主要内容

事件系统

React 基于 Virtual DOM 实现了一个 SyntheticEvent (合成事件)层,我们所定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,它完全符合 W3C 标准,不会存在任何 IE 标 准的兼容性问题。

事件绑定方式:

<botton onClick={this.handClick}>Test</button>

合成事件的实现机制

在 React 底层,主要对合成事件做了两件事:事件委派和自动绑定。

  1. 事件委派

在使用 React 事件前,一定要熟悉它的事件代理机制。它并不会把事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率 也有很大提升。

  1. 自动绑定

在 React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。 而且 React 还会对这种引用进行缓存,以达到 CPU 和内存的最优化。在使用 ES6 classes 或者纯函数时,这种自动绑定就不复存在了,我们需要手动实现 this 的绑定。

  • bind 方法绑定 this
  • 构造器内声明
  • 箭头函数

事件注册

阅读:深入理解 React:事件机制原理

React 的事件注册过程中主要做了 2 件事: document 上注册、存储事件回调。

事件注册

document 上注册

在 React 组件挂载阶段,根据组件内的声明的事件类型(click、change)等,在 document 上注册事件(使用 addEventListener 注册),并且指定统一的回调函数 dispatchEvent(事件监听器:该事件监听器维持一个映射保存所有组件内部的事件监听和处理函数)。换句话说,document 不管注册什么事件、都具有统一的回调函数 dispatchEvent。也正是因为这一事件委托机制,都具有相同的 dispatchEvent,所以对于同一种事件类型,不论在 document 上注册了几次,最终也只会保留一个有效实例,这样能减少内存开销。

解释: 在一个组件所有的同类型事件,比如所有的点击事件,将在 document 维持一个 dispatchEvent: document.addEventListener('click', dispatchEvent),从这里也可以看出 React 的事件是在 DOM 事件流的冒泡阶段被触发执行。

存储事件回调

React 为了在触发事件时可以查找到对应的回调去执行,会把组件内的所有事件统一地存放到一个对象中(listenerBank)。而存储方式如上图,首先会根据事件类型分类存储,例如 click 事件相关的统一存储在一个对象中,回调函数的存储采用键值对(key/value)的方式存储在对象中,key 是组件的唯一标识 id,value 对应的就是事件的回调函数。

在《深入 React 技术栈》中,dispatchEvent 和 listenerBank 被统称为事件监听器,该事件监听器维持一个映射保存所有组件内部的事件监听和处理函数。

合成事件

事件分发(事件触发)

React 的事件触发只会发生在 DOM 事件流的冒泡阶段,因为在 document 上注册时就默认是在冒泡阶段被触发执行。

具体流程:

  1. 触发事件,开始 DOM 事件流,先后经过三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段;
  2. 当事件冒泡到 document 时,触发统一的事件分发函数 ReactEventListener.dispatchEvent;
  3. 根据原生事件对象(nativeEvent)找到当前节点(即事件触发节点)对应的 ReactDOMComponent 对象;
  4. 事件的合成 4.1 根据当前事件类型生成对应的合成对象; 4.2 封装原生事件对象和冒泡机制; 4.3 查找当前元素以及它所有父级; 4.4 在 listenerBank 中查找事件回调函数并合成到 events 中;
  5. 批量执行合成事件(events)内的回调函数;
  6. 如果没有阻止冒泡,会将继续进行 DOM 事件流的冒泡(从 document 到 window),否则结束事件触发.

合成事件-流程

注:上图中阻止冒泡是指调用 stopImmediatePropagation 方法阻止冒泡,如果是调用 stopPropagation 阻止冒泡,document 上如果还注册了同类型其他的事件,也将会被触发执行,但会正常阻断 window 上事件触发。了解两者之间的详细区别

React 中的原生事件

在 React 中使用 DOM 原生事件时,一定要在组件卸载时手动移除,否则很可能出现内存泄漏的问题。

  1. 合成事件和原生事件混用

在 Web 页面中添加一个使用移动设备扫描二维码的功能,在点击按钮时显示二维码,点击非二维码区域时将其隐藏起来。示例代码如下:

import React, { Component } from 'react';
import qrImg from '../../assets/images/logo.png';
class QrCode extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickQr = this.handleClickQr.bind(this);
this.state = { active: false };
}
componentDidMount() {
// 我们无法在组件中将事件绑定到 body 上,因为 body 在组件范围之外,只能使用原生绑定事件来实现。
document.body.addEventListener('click', e => {
this.setState({ active: false });
});
}
componentWillUnmount() {
// 移除原生事件监听
document.body.removeEventListener('click');
}
handleClick() {
this.setState({
active: !this.state.active
});
}
handleClickQr(e) {
e.stopPropagation();
}
render() {
return (
<div className='qr-wrapper'>
<button className='qr' onClick={this.handleClick}>
二维码
</button>{' '}
<div className='code' style={{ display: this.state.active ? 'block' : 'none', backgroundColor: 'red' }} onClick={this.handleClickQr}>
<img src={qrImg} alt='qr' />
</div>{' '}
</div>
);
}
}
export default QrCode;

实际效果是在你点击二维码区域时二维码依然会隐藏起来。原因也很简单,就是 React 合成事件系统的委托机制,在合成事件内部仅仅对最外层的容器进行了绑定,并且依赖事件的冒泡机制完成了委派。也就是说,事件并没有直接绑定到 div.qr 元素上,所以在这里使用 e.stopPropagation() 并没有用。当然,解决 方法也很简单。

方法一——不要将合成事件与原生事件混用:

componentDidMount() {
// 我们无法在组件中将事件绑定到 body 上,因为 body 在组件范围之外,只能使用原生绑定事件来实现。
document.body.addEventListener('click', e => {
this.setState({ active: false });
});
// 解决点击二维码隐藏起来的bug
document.querySelector('.code').addEventListener('click', e => {
e.stopPropagation();
});
}
componentWillUnmount() {
// 移除原生事件监听
document.body.removeEventListener('click');
document.querySelector('.code').removeEventListener('click');
}

方法二——通过 e.target 判断来避免:

componentDidMount() {
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({
active: false
});
});
}

小结

React 合成事件和原生 DOM 事件的主要区别:

  1. React 组件上声明的事件没有绑定在 React 组件对应的原生 DOM 节点上。
  2. React 利用事件委托机制,将几乎所有事件的触发代理(delegate)在 document 节点上,事件对象(event)是合成对象(SyntheticEvent),不是原生事件对象,但通过 nativeEvent 属性访问原生事件对象。
  3. 由于 React 的事件委托机制,React 组件对应的原生 DOM 节点上的事件触发时机总是在 React 组件上的事件之前。